package com.javabi.sizeof;

import java.io.PrintStream;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.Set;

/**
 * A collection of memory utility methods.
 */
public final class MemoryUtil {

	/** The number of bytes used to represent a byte. */
	public static final int SIZE_OF_BYTE = 1;
	/** The number of bytes used to represent a byte. */
	public static final int SIZE_OF_SHORT = 2;
	/** The number of bytes used to represent a byte. */
	public static final int SIZE_OF_INT = 4;
	/** The number of bytes used to represent a byte. */
	public static final int SIZE_OF_LONG = 8;

	/** The number of bytes used to represent a boolean. */
	public static final int SIZE_OF_BOOLEAN = 1;
	/** The number of bytes used to represent a char. */
	public static final int SIZE_OF_CHAR = 2;
	/** The number of bytes used to represent a byte. */
	public static final int SIZE_OF_FLOAT = 4;
	/** The number of bytes used to represent a byte. */
	public static final int SIZE_OF_DOUBLE = 8;

	/** The number of bytes used to represent a pointer field (unassigned/null). */
	public static final int SIZE_OF_NULL_POINTER = 4;
	/** The number of bytes used to represent an object (with no fields). */
	public static final int SIZE_OF_OBJECT_INSTANCE = 8;

	/**
	 * Returns the total memory the JVM has allocated
	 * @return the total memory the JVM has allocated.
	 * @see Runtime#totalMemory()
	 */
	public static final long total() {
		return Runtime.getRuntime().totalMemory();
	}

	/**
	 * Returns the maximum memory the JVM can allocate
	 * @return the maximum memory the JVM can allocate.
	 * @see Runtime#maxMemory()
	 */
	public static final long max() {
		return Runtime.getRuntime().maxMemory();
	}

	/**
	 * Returns the memory free.
	 * @return the memory free.
	 * @see Runtime#freeMemory()
	 */
	public static final long free() {
		return Runtime.getRuntime().freeMemory();
	}

	/**
	 * Returns the memory used (approximate).
	 * @return the memory used.
	 * @see Runtime#totalMemory()
	 * @see Runtime#freeMemory()
	 */
	public static final long used() {
		Runtime runtime = Runtime.getRuntime();
		return runtime.totalMemory() - runtime.freeMemory();
	}

	/**
	 * Returns the closest multiple of 8 of the given size (rounding up).
	 * @param size the size.
	 * @return the multiple.
	 */
	private static final long roundUp(long size) {
		if (size % SIZE_OF_OBJECT_INSTANCE == 0) {
			return size;
		}
		return ((size / SIZE_OF_OBJECT_INSTANCE) + 1) * SIZE_OF_OBJECT_INSTANCE;
	}

	/**
	 * Returns the approximate size (in bytes) of the given object in bytes.
	 * <p>
	 * Find out the size of an object during runtime. The algorithm works by using reflection to recursively traverse the objects fields, calculating the size of each object encountered, returning the
	 * total. The instance set is optional and is used to record all instances of objects already encountered, preventing circular dependencies. The definition map determines the size of any object
	 * encountered and can be configured to ignore certain classes, or even alter the algorithm for calculating the size of a class.
	 * </p>
	 * <ul>
	 * <li>Cannot distinguish {@link String#intern()} Strings, treats all instances as non-canonical.</li>
	 * <li>The most common error is {@link StackOverflowError}, occurring when the object graph is too deep or an object is a particularly long linked list (for example {@link LinkedList} or
	 * {@link LinkedHashMap}). A special definition will be required to deal with classes that cause this error.</li>
	 * </ul>
	 * @param object the object.
	 * @param definitionMap the class definitions.
	 * @param instanceSet the instance set.
	 * @return the size in bytes.
	 */
	public static final <T> long sizeOf(T object, ClassDefinitionMap definitionMap, Set<Object> instanceSet) {
		if (object == null) {
			return 0;
		}
		if (instanceSet != null && !instanceSet.add(object)) {
			return 0;
		}

		Class<T> type = (Class<T>) object.getClass();
		ClassDefinition<T> definition = definitionMap.get(type);
		long size = definition.sizeOf(object);

		// Elements
		if (definition.hasElements(object)) {
			size = definition.sizeOfElements(object, definitionMap, instanceSet, size);
		}
		size = roundUp(size);

		// Done
		return size;
	}

	/**
	 * Returns the approximate size (in bytes) of the given object.
	 * @param object the object.
	 * @return the size in bytes.
	 */
	public static final long sizeOf(Object object) {
		if (object == null) {
			return 0;
		}
		return sizeOf(object, new ClassDefinitionMap(), new IdentityHashSet());
	}

	/**
	 * Returns the approximate size (in bytes) of the given object in bytes.
	 * @param object the object.
	 * @param definitionMap the class definitions.
	 * @param instanceSet the instance set.
	 * @param stream the debug print stream.
	 * @return the size in bytes.
	 */
	public static final <T> long sizeOfDebug(T object, ClassDefinitionMap definitionMap, Set<Object> instanceSet, PrintStream stream) throws IllegalAccessException {
		if (object == null) {
			return 0;
		}
		if (instanceSet != null && !instanceSet.add(object)) {
			return 0;
		}

		Class<T> type = (Class<T>) object.getClass();
		ClassDefinition<T> definition = definitionMap.get(type);
		long rawSize = definition.sizeOfDebug(object, definitionMap, instanceSet, stream);
		long roundedSize = roundUp(rawSize);

		// Done
		return roundedSize;
	}

	/**
	 * Returns the approximate size (in bytes) of the given object.
	 * @param object the object.
	 * @param stream the debug print stream.
	 * @return the size in bytes.
	 */
	public static final long sizeOfDebug(Object object, PrintStream stream) throws IllegalAccessException {
		if (object == null) {
			return 0;
		}
		return sizeOfDebug(object, new ClassDefinitionMap(), new IdentityHashSet(), stream);
	}

	/**
	 * Inaccessible constructor.
	 */
	private MemoryUtil() {
	}

}
